web components - 02

revision:


Content

What is a Web Component? Custom elements are HTML elements, like <div>, <section> or <article> The shadow DOM is an encapsulated version of the DOM. HTML template Web Components - examples Web Components - more complex examples


What is a Web Component?

top

A Web Component is a way to create an encapsulated, single-responsibility code block, which can be reused on any page.

Web Components consist of three separate technologies that are used together:

custom elements : fully-valid HTML elements with custom templates, behaviors and tag names (e.g. <one-dialog>) made with a set of JavaScript APIs. Custom elements are defined in the HTML Living Standard specification.
shadow DOM : capable of isolating CSS and JavaScript, almost like an <iframe>. This is defined in the Living Standard DOM specification.
HTML templates : user-defined templates in HTML that aren't rendered until called upon. The <template> tag is defined in the HTML Living Standard specification.

Each of the technologies can be used independently or combined with any of the others. In other words, they are not mutually exclusive.


Custom elements are HTML elements, like <div>, <section> or <article>

top

They are something we can name ourselves that are defined via a browser API.

Custom elements are just like those standard HTML elements — names in angle brackets — except they always have a dash in them, like <news-slider> or <bacon-cheeseburger>.

Browser vendors have committed not to create new built-in elements containing a dash in their names to prevent conflicts.

Custom elements contain their own semantics, behaviors, markup and can be shared across frameworks and browsers.

There are two kinds of custom elements:

autonomous custom elements : “all-new” elements, extending the abstract HTMLElement class.
customized built-in elements : extending built-in elements, like a customized button, based on HTMLButtonElement etc.

examples

code:
                    <div>
                        <my-component></my-component>
                    </div>
                    <script>
                        class MyComponent extends HTMLElement {
                            // connect component
                            connectedCallback() {
                                this.innerHTML = `<h1>Hello world</h1>`;
                            }
                        }
                        // register component
                        customElements.define('my-component', MyComponent);
                    </script>
                

explanation:

In this example, we define <my-component>, our very own HTML element. It doesn't do much, however this is the basic building block of a custom element.
All custom elements must in some way extend an HTMLElement in order to be registered with the browser.

To do anything useful, the class requires a method named connectedCallback() which is invoked when the element is added to a document.

The class must be registered with the CustomElementRegistry to define it as a handler for a specific element.

The browser now associates the <hello-world> element with the MyComponent class when your JavaScript is loaded (e.g. <script type="module" src="./mycomponent.js"></script>).

Custom elements exist without third-party frameworks and the browser vendors are dedicated to the continued backward compatibility of the spec, all but guaranteeing that components written according to the specifications will not suffer from breaking API changes.

Creating a custom element from scratch

The customElements API gives us a path to define custom HTML tags that can be used in any document that contains the defining class.

Essentially, a custom element consists of two pieces: a tag name and a class that extends the built-in HTMLElement class. The most basic version of a custom element would look like this:

            class HelloWorld extends HTMLElement {
                // connect component
                connectedCallback() {
                  this.textContent = "Hello, World!";
                }
            }
            customElements.define('hello-world', HelloWorld);
        

explanation:

In the example above, we defined a new standards-compliant HTML element, <hello-world></hello-world>. It doesn't do much… yet. For now, using the <hello-world> tag in any HTML document will create a new element with a textContent reading “Hello, World!”.

Adding attributes

Like any other element, we can add HTML attributes:

<hello-world name="Ann"></hello-world>

This could override the text so "Hello Ann!" is displayed. To achieve this, you can add a constructor() function to the HelloWorld class, which is run when each object is created.
It must: 1/call the super() method to initialize the parent HTMLElement, and 2/ make other initializations. In this case, we'll define a name property that is set to a default of “World”:

            class HelloWorld extends HTMLElement {
                constructor() {
                super();
                this.name = 'World';
                }
   
                // more code...
            }
        

A static observedAttributes() property should return an array of properties to observe:

            // component attributes
            static get observedAttributes() {
              return ['name'];
            }
        

An attributeChangedCallback() method is called when an attribute is defined in the HTML or changed using JavaScript. It's passed the property name, old value, and new value:

            // attribute change
            attributeChangedCallback(property, oldValue, newValue) {
                if (oldValue === newValue) return;
                this[ property ] = newValue;
            }
        

Finally, you need to tweak the message in the connectedCallback() method:

            // connect component
            connectedCallback() {
                this.textContent = `Hello ${ this.name }!`;
            }
        

Lifecycle methods

The browser automatically calls six methods throughout the lifecycle of the Web Component state.

constructor() : is called when the component is first initialized. It must call super() and can set any defaults or perform other pre-rendering processes.

static get observedAttributes : returns an array of attributes that the browser will monitor for changes.

attributeChangedCallback(propertyName, oldValue, newValue) : called whenever an observed attribute is changed. Those defined in HTML are passed immediately, but JavaScript can modify them. The method may need to trigger a re-render when this occurs.

connectedCallback(): browser calls this method when Web Component is appended to a Document Object Model (DOM). It should run any required rendering. It can be called many times if an element is repeatedly added/removed.

disconnectedCallback() : browser calls this method when the Web Component is removed from a Document Object Model (DOM). This may be useful if you need to clean up, such as removing stored state or aborting Ajax requests. It can be called many times if an element is repeatedly added/removed.

adoptedCallback() : this function is called when a Web Component is moved from one document to another. This happens in document.adoptNode, but is very rarely used.

Syntax:

            class HelloWorld extends HTMLElement {
                static get observedAttributes() {
                  return ['open'];
                }
                
                attributeChangedCallback(attrName, oldValue, newValue) {
                  if (newValue !== oldValue) {
                    this[attrName] = this.hasAttribute(attrName);
                  }
                }
                
                connectedCallback() {
                  const template = document.getElementById('hellow-world');
                  const node = document.importNode(template.content, true);
                  this.appendChild(node);
                }
              }
        

The connectedCallback is separate from the element's constructor.

Whereas the constructor is used to set up the bare bones of the element, the connectedCallback is typically used for adding content to the element, setting up event listeners or otherwise initializing the component.
The constructor can't be used to modify or manipulate the element's attributes by design. If we were to create a new instance of our dialog using document.createElement, the constructor would be called. A consumer of the element would expect a simple node with no attributes or content inserted.
The createElement function has no options for configuring the element that will be returned. It stands to reason, then, that the constructor shouldn't have the ability to modify the element that it creates. That leaves us with the connectedCallback as the place to modify our element.


The shadow DOM is an encapsulated version of the DOM.

top

The shadow DOM is capable of isolating CSS and JavaScript

Selectors and styles inside of a shadow DOM node don't leak outside of the shadow root and styles from outside the shadow root don't leak in. There are a few exceptions that inherit from the parent document, like font family and document font sizes (e.g. rem) that can be overridden internally.

All shadow roots still exist in the same document so that all code can be written inside a given context but no worry about conflicts with other styles or selectors.

This allows authors to effectively isolate DOM fragments from one another, including anything that could be used as a CSS selector and the styles associated with them.
Any content inside of the document's scope is referred to as the "light DOM", and anything inside a shadow root is referred to as the "shadow DOM".

When using the light DOM, an element can be selected by using document.querySelector('selector') or by targeting any element's children by using element.querySelector('selector').

In the same way, a shadow root's children can be targeted by calling shadowRoot.querySelector where shadowRoot is a reference to the document fragment — the difference being that the shadow root's children will not be select-able from the light DOM.

For example, If we have a shadow root with a <button> inside of it, calling shadowRoot.querySelector('button') would return our button, but no invocation of the document's query selector will return that element because it belongs to a different DocumentOrShadowRoot instance. Style selectors work in the same way.

In this respect, the shadow DOM works sort of like an <iframe> where the content is cut off from the rest of the document; however, when we create a shadow root, we still have total control over that part of our page, but scoped to a context. This is what we call encapsulation.

examples

This will use the CSS background
code:
                    <div>
                        <div id="example">This will use the CSS background</div>
                        <button id="button">Not tomato</button>
                    </div>
                    <script>
                            const shadowRoot = document.getElementById('example').attachShadow({ mode: 'open' });
                            shadowRoot.innerHTML = `<style>
                            button {
                                background: tomato;
                                color: white;
                            }
                            </style>
                            <button id="button"><slot></slot> tomato</button>`;
                    </script>
                

A shadow root can also include content from its containing document by using the <slot> element. Using a slot will drop user content from the outer document at a designated spot in your shadow root.

The shadow DOM attaches a separated DOM to the Web Component with elem.attachShadow({mode: ...}):

            const shadow = this.attachShadow({ mode: 'closed' });
        

The mode can be:

"open" : JavaScript in the outer page can access the shadow DOM (using Element.shadowRoot);
"closed" : the shadow DOM can only be accessed within the Web Component.

The shadow DOM can be manipulated like any other DOM element:

            connectedCallback() {
                const shadow = this.attachShadow({ mode: 'closed' });
                 shadow.innerHTML = `
                  <style>
                    p {
                      text-align: center;
                      font-weight: normal;
                      padding: 1em;
                      margin: 0 0 2em 0;
                      background-color: #eee;
                      border: 1px solid #666;
                    }
                  </style>
                <p>Hello ${ this.name }!</p>`;
            }
        

explanation:

The component renders the “Hello” text inside a <p> element and styles it. It cannot be modified by JavaScript or CSS outside the component, although some styles such as the font and color are inherited from the page because they were not explicitly defined. The styles scoped to this Web Component cannot affect other paragraphs on the page or even other <hello-world> components.

Note that the CSS :host selector can style the outer <hello-world> element from within the Web Component.


HTML template

top

HTML <template> allows to stamp out re-usabe templates of code inside a normal HTML flow, which won't be immediately rendered, but can be used at a later time.

A built-in <template> element serves as a storage for HTML markup templates. The browser ignores its contents, only checks for syntax validity, but we can access and use it in JavaScript, to create other elements.

Its content can be any valid HTML, even if it normally requires a proper enclosing tag. We can put styles and scripts into <template> as well.

The content becomes live (styles apply, scripts run etc) when we insert it into the document.

examples

code:
                    <div>
                        <template id="book-template">
                            <li><span class="title"></span> — <span class="author"></span></li>
                        </template>
                        <ul id="books"></ul>
                    </div>
                    <script>
                        const fragment = document.getElementById('book-template');
                        const books = [
                            { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
                            { title: 'A Farewell to Arms', author: 'Ernest Hemingway' },
                            { title: 'Catch 22', author: 'Joseph Heller' }
                        ];
            
                        books.forEach(book => {
                        // Create an instance of the template content
                        const instance = document.importNode(fragment.content, true);
                        // Add relevant content to the template
                        instance.querySelector('.title').innerHTML = book.title;
                        instance.querySelector('.author').innerHTML = book.author;
                        // Append the instance ot the DOM
                        document.getElementById('books').appendChild(instance);
                        });
                    </script>
                

explanation:

The example above wouldn't render any content until a script has consumed the template, instantiated the code and told the browser what to do with it.

Crafting reusable HTML templates

The template elements are "user-defined templates in HTML that aren't rendered until called upon." In other words, a template is HTML that the browser ignores until told to do otherwise.

These templates then can be passed around and reused in a lot of interesting ways.

As simple as it might sound, a <template> is an HTML element, so the most basic form of a template with content would be:

            <template>
                <h1>Hello world</h1>
            </template>
        

Running this in a browser would result in an empty screen as the browser doesn't render the template element's contents. This becomes incredibly powerful because it allows us to define content (or a content structure) and save it for later — instead of writing HTML in JavaScript. In order to use the template, we will need JavaScript.

examples

code:
                    <div>
                        <template id="template-1">
                            <h4>Hello fabulous blue planet!</h4>
                        </template>
                    </div>
                    <script>
                        const template = document.querySelector('#template-1');
                        const node = document.importNode(template.content, true);
                        document.body.appendChild(node);
                    </script> 
                

The real magic happens in the document.importNode method. This function will create a copy of the template's content and prepare it to be inserted into another document (or document fragment). The first argument to the function grabs the template's content and the second argument tells the browser to do a deep copy of the element's DOM subtree (i.e. all of its children).

We could have used the template.content directly, but in so doing we would have removed the content from the element and appended to the document's body later. Any DOM node can only be connected in one location, so subsequent uses of the template's content would result in an empty document fragment (essentially a null value) because the content had previously been moved. Using document.importNode allows us to reuse instances of the same template content in multiple locations.

That node is then appended into the document.body and rendered for the user. This ultimately allows us to do interesting things, like providing our users (or consumers of our programs) templates for creating content.

The versatility of template

Templates can contain any HTML. That includes script and style elements.

examples

A very simple example would be a template that appends a button that alerts us when it is clicked.

code:
                    <div>
                        <template id="template-2">
                            <script>
                                const button = document.getElementById('click-me');
                                button.addEventListener('click', event => alert(event));
                            </script>
                            <style>
                                button#click-me {all: unset; background: tomato; border: 0; border-radius: 0.4vw; color: white; font-family: Helvetica; font-size: 1.5vw; padding: .5vw 1vw;}
                            </style>
                            <button id="click-me">Log click event</button>
                        </template>    
                    </div>
                    <script>
                        'use strict';
            
                        const template_2 = document.getElementById('template-2');
                        document.body.appendChild(
                        document.importNode(template_2.content, true)
                        );
                    </script> 
                

explanation:

Once this element is appended to the DOM, we will have a new button with ID #click-me, a global CSS selector targeted to the button's ID, and a simple event listener that will alert the element's click event. For our script, we simply append the content using document.importNode and we have a mostly-contained template of HTML that can be moved around from page to page.

Template slots

Slots allow you to customize a template. Presume you wanted to use your <hello-world> Web Component but place the message within a <h1> heading in the shadow DOM. You could write this code:

            <hello-world name="Ann">
                <h1 slot="msgtext">Hello Default!</h1>
                <p>This text will become part of the component.</p>
            </hello-world>
            

A <slot> element in the shadow DOM points to the inserted elements. You can only access them by locating a <slot> then using the .assignedNodes() method to return an array of inner children.


Web Components - examples

top
examples
code:
                    <div>
                        <div id="DIV-A"></div>
                        <template id="my-paragraph"><p id="P-one">My first paragraph</p></template>   
                    </div>
                    <style>
                        p#P-one { color: lightblue; background-color: #666; padding: 5px;}
                    </style>
                    <script>
                        let template_A = document.getElementById('my-paragraph');
                        let templateContent = template_A.content;
                        document.getElementById('DIV-A').appendChild(templateContent);
                    </script>
                

examples
code:
                    <div>
                        <div id="DIV-B"></div>     
                        <template id="paragraph-one"><p id="P-two"><slot name="my-text">My default text</slot></p></template>
                    </div>
                    <style>
                        p#P-two { color: lightgreen; background-color: #666; padding: 5px;}
                    </style>
                    <script>
                        let template1 = document.getElementById('paragraph-one');
                        let templateContent1 = template1.content;
                        document.getElementById("DIV-B").appendChild(templateContent1);
                    </script>
                

examples
code:
                    <div>
                        <div id="DIV-C"></div>
                        <template id="paragraph-two">
                        <style>
                            p#P-three { color: orange; background-color: #666; padding: 5px;}
                        </style>
                        <p id="P-three"><slot name="text-one">My second default text</slot></p>
                        </template>
                    </div>
                    <script>
                        let template2 = document.getElementById('paragraph-two');
                        let templateContent2 = template2.content;
                        document.getElementById("DIV-C").appendChild(templateContent2);
                    </script>
                

examples
Let's have some different text!
code:
                    <div>
                        <div id=""DIV-D></div>
                        <my-paragraph id="paragraph-three">
                            <span id="span_1" slot="text-two">Let's have some different text!</span>
                        </my-paragraph>
                    </div>
                    <style>
                        #span_1 { color: orange; background-color: #666; padding: 5px;}
                    </style>
                    <script>
                        customElements.define('paragraph-three',
                            class extends HTMLElement {
                                constructor() {
                                    super();
                                    const template = document.getElementById('paragraph-three');
                                    const templateContent = template.content;
                                    this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
                                }
                            }
                        ); 
                    </script>
                

examples
  • Let's have still a somewhat different text!
  • In a list!
code:
                    <div>
                        <my-paragraph id="para-four">
                            <ul id="ul-1" slot="my-text">
                                <li>Let's have still a somewhat different text!</li>
                                <li>In a list!</li>
                            </ul>
                        </my-paragraph>
                    </div>
                    <style>
                    #ul-1 { color: orange; background-color: #666; padding: 5px;}
                </style>
                    <script>
                        customElements.define('para-four',
                            class extends HTMLElement {
                                constructor() {
                                    super();
                                    const template = document.getElementById('para-four');
                                    const templateContent = template.content;
                                    this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
                                }
                            }
                        ); 
                    </script>
                

examples
code:
                    <div>
                        <my-paragraph>
                            <add-text id="text-1"></add-text>
                        </my-paragraph>
                    </div>
                    <script>
                        class AddText extends HTMLElement {
                            constructor() {
                                super();
                                var host = this.attachShadow({mode: 'open'});
                                const div=document.createElement('div');
                                    div.setAttribute('class', 'main');
                                const p=document.createElement('p');
                                    p.textContent='Web Components are awesome, what do you think?';
                                const style=document.createElement('style');
                                    style.textContent=' p {color:burlywood; background-color: green; padding:0.5vw;} ';
                                    host.appendChild(style);
                                    host.appendChild(div);
                                    div.appendChild(p);
                            }
                        }
                        customElements.define('add-text', AddText);
                        var host = document.querySelector(".main");
                        var root = host.createShadowRoot();
                        root.innerHTML = '<p> <strong>Web Components</strong> are awesome, what do you think?</p>'
                    </script>
                

Web Components - more complex examples

top

enter some details

examples


code:
                    <div>
                        <form>
                            <div>
                                <label for="cvc">Enter your CVC <popup-info img="../pics/download.png" data-text=
                                "Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers 
                                on the back of your card."></popup-info></label>
                            <input type="text" id="cvc">
                            </div>
                        </form>
                        <br><br>
                    </div>
                    <script>
                        // Create a class for the element
                        class PopUpInfo extends HTMLElement {
                            constructor() {
                            // Always call super first in constructor
                            super();
                            // Create a shadow root
                            const shadow = this.attachShadow({mode: 'open'});
                            // Create spans
                            const wrapper = document.createElement('span');
                                wrapper.setAttribute('class', 'wrapper');
                            const icon = document.createElement('span');
                                icon.setAttribute('class', 'icon');
                                icon.setAttribute('tabindex', 0);
                            const info = document.createElement('span');
                            info.setAttribute('class', 'info');
                            // Take attribute content and put it inside the info span
                            const text = this.getAttribute('data-text');
                                info.textContent = text;
                            // Insert icon
                            let imgUrl;
                                if(this.hasAttribute('img')) {
                                imgUrl = this.getAttribute('img');
                                } else {
                                imgUrl = 'default.png';
                            }
                            const img = document.createElement('img');
                                img.src = imgUrl;
                                icon.appendChild(img);
                            // Create some CSS to apply to the shadow dom
                            const style = document.createElement('style');
                            console.log(style.isConnected);
                            style.textContent = `
                                    .wrapper {position: relative;}
                                    .info {font-size: 0.8rem;  width: 200px; display: inline-block; border: 1px solid black; 
                                        padding: 10px; background: white; border-radius: 10px; opacity: 0; transition: 0.6s all; 
                                        position: absolute; bottom: 20px; left: 10px;       z-index: 3; }
                                    img {width: 1.2rem;}
                                    .icon:hover + .info, .icon:focus + .info {opacity: 1;}
                            `;
                            // Attach the created elements to the shadow dom
                            shadow.appendChild(style);
                            console.log(style.isConnected);
                            shadow.appendChild(wrapper);
                            wrapper.appendChild(icon);
                            wrapper.appendChild(info);
                        }
                        }
                        // Define the new element
                        customElements.define('popup-info', PopUpInfo);
                    </script>
                

expanding list web component

examples
  • UK
    • Yorkshire
      • Leeds
        • Train station
        • Town hall
        • Headrow
      • Bradford
      • Hull
  • USA
    • California
      • Los Angeles
      • San Francisco
      • Berkeley
    • Nevada
    • Oregon
  • Not
  • an
  • expanding
  • list
code:
                    <div>
                        <ul is="expanding-list">
                            <li>UK
                                <ul>
                                    <li>Yorkshire
                                        <ul>
                                            <li>Leeds
                                                <ul>
                                                    <li>Train station</li>
                                                    <li>Town hall</li>
                                                    <li>Headrow</li>
                                                </ul>
                                            </li>
                                            <li>Bradford</li>
                                            <li>Hull</li>
                                        </ul>
                                    </li>
                                </ul>
                            </li>
                            <li>USA
                                <ul>
                                    <li>California
                                        <ul>
                                            <li>Los Angeles</li>
                                            <li>San Francisco</li>
                                            <li>Berkeley</li>
                                        </ul>
                                    </li>
                                    <li>Nevada</li>
                                    <li>Oregon</li>
                                </ul>
                            </li>
                        </ul>
            
                        <ul>
                            <li>Not</li>
                            <li>an</li>
                            <li>expanding</li>
                            <li>list</li>
                        </ul>
                    </div>
                    <script>
                        // Create a class for the element
                        class ExpandingList extends HTMLUListElement {
                            constructor() {
                            // Always call super first in constructor; return value from super() is a reference to this element
                            self = super();
            
                            // Get ul and li elements that are a child of this custom ul element
                            // li elements can be containers if they have uls within them
                            const uls = Array.from(self.querySelectorAll('ul'));
                            const lis = Array.from(self.querySelectorAll('li'));
            
                            // Hide all child uls
                            // These lists will be shown when the user clicks a higher level container
                            uls.forEach(ul => {
                            ul.style.display = 'none';
                            });
            
                                // Look through each li element in the ul
                            lis.forEach(li => {
                            // If this li has a ul as a child, decorate it and add a click handler
                            if (li.querySelectorAll('ul').length > 0) {
                                // Add an attribute which can be used  by the style
                                // to show an open or closed icon
                                li.setAttribute('class', 'closed');
            
                                // Wrap the li element's text in a new span element
                                // so we can assign style and event handlers to the span
                                const childText = li.childNodes[0];
                                const newSpan = document.createElement('span');
            
                                // Copy text from li to span, set cursor style
                                newSpan.textContent = childText.textContent;
                                newSpan.style.cursor = 'pointer';
                                
                                // Add click handler to this span
                                newSpan.onclick = self.showul;
                                
                                // Add the span and remove the bare text node from the li
                                childText.parentNode.insertBefore(newSpan, childText);
                                childText.parentNode.removeChild(childText);
                            }
                            });
                        }
            
                        // li click handler
                        showul = function (e) {
                            // next sibling to the span should be the ul
                            const nextul = e.target.nextElementSibling;
            
                            // Toggle visible state and update class attribute on ul
                            if (nextul.style.display == 'block') {
                            nextul.style.display = 'none';
                            nextul.parentNode.setAttribute('class', 'closed');
                            } else {
                            nextul.style.display = 'block';
                            nextul.parentNode.setAttribute('class', 'open');
                            }
                        };
                        }
            
                        // Define the new element
                        customElements.define('expanding-list', ExpandingList, { extends: 'ul' });
                    </script>
                

another custom element

examples
code:
                    <div>
                        <custom-element foo="foo" bar="bar" baz="baz"></custom-element>
                    </div>
                    <script>
                        class BlinkElement extends HTMLElement {
                            constructor() {
                                super();
                            }
                
                            connectedCallback() {
                                const shadow = this.attachShadow({mode: 'open'});
                                this.span = document.createElement('span');
                                this.span.textContent = this.getAttribute('text');
                                const style = document.createElement('style');
                                style.textContent = 'span { color: black; background-color: lightblue; margin-left: 2vw; font-size: 2vw; }';
                                this.intervalTimer = setInterval(() => {
                                let styleText = this.style.textContent;
                                if (style.textContent.includes('red')) {
                                    style.textContent = 'span { color: black }';
                                } else {
                                    style.textContent = 'span { color: red }';
                                }
                
                            }, 2000)
                                shadow.appendChild(style);
                                shadow.appendChild(this.span);
                            }
                
                            disconnectedCallback() {
                                clearInterval(this.intervalTimer);
                            }
                
                            static get observedAttributes() {
                                return ['text'];
                            }
                
                            attributeChangedCallback(name, oldValue, newValue) {
                                if (name === 'text') {
                                if (this.span) {
                                    this.span.textContent = newValue;
                                }
                
                                }
                            }
                
                            
                        }
                        const blink2 = document.createElement('blink-element');
                        document.body.appendChild(blink2);
                        blink2.setAttribute('text', 'bar');
                        blink2.setAttribute('text', 'baz');
                
                        // const blink2 = document.createElement('blink-element');
                        // document.body.appendChild(blink2);
                        // blink2.setAttribute('text', 'bar');
                        // document.body.removeChild(blink2);
                
                        customElements.define('blink-element', BlinkElement);
                    </script>